home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Turnbull China Bikeride
/
Turnbull China Bikeride - Disc 2.iso
/
STUTTGART
/
LANG
/
C
/
LIB
/
OSLIB
/
EXAMPLES
/
doc
/
x
Wrap
Text File
|
1995-09-06
|
12KB
|
297 lines
The exception library
--- --------- -------
When writing code in BASIC, we are used to installing error-handlers
to cope with SWI's that generate errors. In C this is less common,
mainly because of the problem of writing nested error-handlers in a
modular way. Nevertheless, there are still advantages to doing so.
In RISC O S, any system call can return an error indicator, of type
|os_error *|, instead of the results that are expected under normal
termination. If the normally-terminating SWI could be considered to be
of type
Entry -> Exit,
where Entry is the type of its entry conditions and Exit the type of its
exit conditions, then the SWI as provided must be considered to be of
type
Entry -> Union (Exit, os_error *).
In general, both Entry asnd Exit are compound types: for example, the
type of pdriver_draw_page() is
Struct (int, os_box *, int, char *, int) -> Struct (bool, int)
(which would be rendered into C as void pdriver_draw_page (int, os_box
*, int, char *, bool *, int *). However, this ignores the fact that it
might return an error instead; so the real type is
Struct (int, os_box *, int, char *, int) ->
Union (Struct (bool, int), os_error *)
which is rendered as os_error *xpdriver_draw_page (int, os_box *, int,
char *, bool *, int *).
The problem with using functions of this form is that every single
return value must be checked, and appropriate action taken if an error
has been returned. This leads to code like
if ((error = xpdriver_draw_page (1, &req, page, NULL, &more,
NULL)) != NULL)
goto finish;
(If you don't like 'goto,' then feel free to imagine there is a call to
an error-handling function there instead. The main argument is not
affected by this.) In effect this "wastes" R0 (the register in which the
result is returned), and the following test, on a condition which hardly
ever occurs.
RISC O S, and the C run-time kernel, provide a mechanism to avoid
this. By calling the non-X form of the SWI, we can ensure that the
normal flow of control of a programme is interrupted when an error
occurs, without having to test for it explicitly. This also means that
the return value of the function can be used to return the most useful
output argument. (Which one this is is a matter of taste, and OSLib
makes this decision in a hopefully-useful way.)
Therefore, the call can be written as
more = pdriver_draw_page (1, &req, page, NULL, NULL);
To do this requires the installation of an error handler. The C run-
time system does some of this for you. An exception library, the x_
module, does the rest.
To use the exception library, code is written in this form:
{ x_exception x;
/statements (1)/
x_TRY (&x)
/statement (2)/
x_CATCH (&x)
/statement (3)/
/statements (4)/
}
The first point is the declaration. To catch an exception locally,
the C run-time system needs some space to store various things. This is
provided in a structure called an |x_exception|. This must be declared
like this---on the stack, not static---or nested error handlers won't
work.
|x_TRY| may be thought of as having a similar syntax to |do|;
|x_CATCH()| to |while|. Both must be present, at the same lexical level,
just as above---if an |x_TRY| is not balanced by an |x_CATCH|, the
effects are highly unpredictable. They must both be provided with
pointers to the same |x_exception| structure. The first statement is
usually a block (in {...}), called the try block, though it could be a
single statement. Exceptions thrown from here cause control to be
transferred to the statement after |x_CATCH()|, called the catch block.
If there are no exceptions, control does not enter the catch block at
all. If any exceptions occur in the statement not in the try block (in
the catch block or outside the |x_TRY ... x_CATCH| construction
altogether), they are just thrown as normal (i e, passed up to enclosing
functions).
After control has passed through |x_TRY()|, |x_RETHROW()| may be used
to propagate the exception further. This would normally be the preferred
course.
This means that within the try block, you are free to put calls to
non-X SWI's. Any SWI error, escape or stack overflow error (any of which
can happen to correct programmes) will be caught and handled locally,
without wasting code on explicit error checking. RISC O S does it for
you!
Normally, any exceptions caught should be rethrown, as in the example
code below, or the code that called you will be unaware of the error
that has occurred. This is an important rule: without it, functions
could end up allocating resources that they would not free, and the
whole purpose of the module would have been subverted. If you think of
the whole calling tree at the moment of the exception, control will pass
to the innermost-nested function that has a catch block; this function
should do whatever tidying up it needs, and then rethrow the exception
to the next enclosing one, and so on until the stack is flat and all
interested functions have had a chance to act.
The stack does not have to be completely flat, however: normally, a
command-line driven programme would just report the error and ask for
another command, and a Wimp-based one would use Wimp_ReportError and
poll again. The application writer has complete flexibility.
Blocks written as above may be nested, either in the try block or in
the catch block, and only functions that need to do tidying-up after
themselves need to use |x_TRY| and |x_CATCH|. Functions that they call
can just "get on with the job:" if they call a SWI, or any other
function, that raises an exception, the exception will just "bubble
back" through all the error handlers, back to the level required by the
application writer.
There are other utilities to round out the module:
|x_THROW_LAST_ERROR| throws the last error detected by the run-time
system. It is normally used after a library call fails. |x_THROW| throws
a given error, in just the same way that the SWI's do (it is just
os_generate_error(), really: either way be used), and x_LOCAL_ERROR
declares one for such use. For example,
if ((file = fopen ("DataFile", "w")) == NULL)
x_THROW_LAST_ERROR ();
/at this point, we know that |file| is a valid handle/
Code to open a file, write to it and close it again would look like
this:
FILE *f;
if ((f = fopen ("DataFile", "w")) == NULL)
x_THROW_LAST_ERROR ();
x_TRY (&x)
{ if (fprintf (f, "Hello, World!\n") < 0)
x_THROW_LAST_ERROR ();
/lots of complicated output to a file, which might go wrong/
}
x_CATCH (&x)
;
if (fclose (f) == NULL)
x_THROW_LAST_ERROR ();
x_RETHROW (&x);
Note the structure:
do something that changes the global state (open file, allocate
memory, etc)
TRY
operate on the changed state (write to file, use memory)
CATCH
;
restore the state (close file, free memory)
rethrow the error
Often, as here, the catch block is empty, since the tidying-up code is
the same in the error case as in the correct case.
x_LOCAL_ERROR() allows a static error block to be constructed: this
can then be thrown as a new error. The effect is exactly the same as if
a SWI was called and threw the error. It should be used at the top level
of a source file. For example, code to check for a text file might look
like this:
x_STATIC_ERROR (Error_Wrong_Type, 1, "File must be of type Text")
fileswitch_object_type obj_type;
bits file_type;
if ((obj_type = osfile_read_stamped_no_path ("Fred", NULL, NULL,
NULL, NULL, &file_type)) != fileswitch_IS_FILE)
osfile_make_error ("Fred", obj_type);
if (file_type != osfile_TYPE_TEXT)
x_THROW (Error_Wrong_Type);
/... if we are here, we have a text file. Conversely, if the file
was not a text file, the right error has been thrown back to
the caller .../
There is also x_GLOBAL_ERROR, which does the same job but without the
'static': it should be used where the error blocks that a source file
uses are part of its interface. In this case, the identifier should also
be declared as an |os_error *| in the source file's accompanying header
file.
There are also functions x_EXIT(), which exits a programme, printing
an error message to |stderr| if one has occurred, and x_REPORT_EXIT()
which exits a programme, calling wimp_report_error() if an error has
occurred. These should only be used in the scope of the |x_exception|
structure|, after a use of |x_TRY|.
There are also functions |x_alloc|, |x_calloc|, |x_free| and
|x_realloc| which are like |malloc|, |calloc|, |free| and |realloc|, but
throw an exception on failure. (It's unfortunate that the relationship
between |x_malloc| and |malloc| is the opposite of that between (say)
|xosmodule_alloc| and |osmodule_alloc|, but it's a logical consequence
of the module naming convention.)
Conclusion
----------
Any SWI can in principal generate an error at any time, for any
reason. These errors are usually associated with the exhaustion of some
resource (memory, file handles, disc space, etc), and code that checks
error-returning SWI's, and in turn returns the error pointer back to its
caller, is time-consuming and inefficient. Using a handler, and error-
generating SWI's, saves 1 or 2 instructions per SWI veneer (the veneer
doesn't have the 'BVS %99; MOV R, #0' or 'MOVVC R, #0'); and typically 2
at each function call ('CMP R0, #0; BNE R0, finish' in the example
above). It also enables more readable code to be produced. For example,
if you check all return codes, a printing loop might look like this:
if ((error = xpdriver_draw_page (1, &req, page, NULL, &more,
NULL)) != NULL)
goto finish;
while (more)
{ if ((error = xcolourtrans_set_gcol (os_COLOUR_BLACK,
colourtrans_SET_FG, os_ACTION_OVERWRITE, NULL, NULL))
!= NULL)
goto finish;
if ((error = xfont_paint (0, t, NONE, 0, 0, NULL, NULL, 0))
!= NULL)
goto finish;
if ((error = xpdriver_get_rectangle (&req, &more, NULL)) !=
NULL)
goto finish;
}
finish:
whereas if you can call x-clear SWI's, you can write instead:
for (more = pdriver_draw_page (1, &req, page, NULL, NULL);
more; more = pdriver_get_rectangle (&req, NULL))
{ colourtrans_set_gcol (os_COLOUR_BLACK, colourtrans_SET_FG,
os_ACTION_OVERWRITE, NULL, NULL);
font_paint (0, t, NONE, 0, 0, NULL, NULL, 0);
}
The first example above compiles to 52 instructions; the second to 44,
for a saving of an enormous 20% in code size (and knock-on effects for
callers). This could also be expected to translate into a 20% speed-up
as well, since all the extra instructions have to be executed. The code
is also substantially easier to write, understand and modify.
The x_ module does not interfere in any way with errors that are not
expected: namely, abort, arithmetic exception, illegal instruction, bad
memory access, termination request. These should probably only be
handled at the very top level in order to provide a last-ditch error
handler: it could do something like saving all unsaved data into a scrap
file.
The C library throws an escape condition when the user presses
Escape, but this behaviour is normally suppressed by the Wimp, except in
special cases like printing. By enabling and disabling escapes around
Wimp_Poll, an application could be easily written to abort its current
operation on Escape and return to its main polling loop, with very
little special code needed, and no risk of files being left open, etc.
The OSLib SWI veneers provide both X and non-X forms of each SWI
function. When writing an application, or a library to be used by
others, there is a big decision to make: do its functions return
|os_error *|, and call X-form SWI's, or do they return a useful result
and call non-X-form SWI's (with error-handlers in the right places)? In
other words, do the library functions mimic x-set SWI's, or x-clear
SWI's?
The libraries and modules described enable us to write better
applications in less time, that run faster and require less memory to
run in.